package it.unibo.refolding.pico.stripchart;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.Sleeper;
import it.unibo.refolding.pico.CommonClRefoldingOptions;
import it.unibo.refolding.pico.CommonRefoldingOptions;
import it.unibo.refolding.pico.ContactPointAndSlopeHelper;
import it.unibo.refolding.pico.DelayedNanoDriverInvocationHandler;
import it.unibo.refolding.pico.FileMovingWatcher;
import it.unibo.refolding.pico.NanoDriver;
import it.unibo.refolding.pico.NanoDriverConfig;
import it.unibo.refolding.pico.NanoDriverUtil;
import it.unibo.refolding.pico.PeakSlopesHelper;
import it.unibo.refolding.pico.PicoUtil;
import it.unibo.refolding.pico.RefoldingUtil;
import it.unibo.refolding.pico.TimedFileContentAndTargetFile;
import it.unibo.refolding.pico.Wlc;
import it.unimi.dsi.fastutil.doubles.DoubleList;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Locale;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.Parser;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;

public class FetchRefoldBreak {

  private static final Logger logger = Logger.getLogger(FetchRefoldBreak.class.getSimpleName());

  public static void main(String[] args) throws ParseException, IllegalAccessException,
    NoSuchFieldException, IOException {
    System.setProperty("java.awt.headless", "true");

    CommonRefoldingOptions commonRefoldingOptions = CommonRefoldingOptions.create();
    Options options = new Options();
    commonRefoldingOptions.addTo(options);

    Option defLsbVsZLsbSlopeOption = PicoUtil.createDefLsbVsZLsbSlopeOption();
    options.addOption(defLsbVsZLsbSlopeOption);

    Option svgDefScaleOption = PicoUtil.createSvgDefScaleOption();
    options.addOption(svgDefScaleOption);

    Option asyncSvgOption = new Option("asyncSvg", true, "");
    options.addOption(asyncSvgOption);

    Option runScriptExtraDelayMillisOption = new Option("runScriptExtraDelayMillis", true, "");
    options.addOption(runScriptExtraDelayMillisOption);
    
    Option initialStartRampNmOption = new Option(
      "initialStartRampNm", true, "Initial value of Start Ramp.");
    initialStartRampNmOption.setRequired(true);
    initialStartRampNmOption.setType(Double.TYPE);
    options.addOption(initialStartRampNmOption);

    Option pushNmOption = new Option(
      "pushNm", true, "How much we need to push, in nm, as detected upon retraction.");
    pushNmOption.setRequired(true);
    pushNmOption.setType(Double.TYPE);
    options.addOption(pushNmOption);

    AnalysisOptions analysisOptions = AnalysisOptions.create();
    analysisOptions.addTo(options);
    
    Option allowedTimeRelativeErrorOption = AnalysisOptions.createAllowedTimeRelativeErrorOption();
    options.addOption(allowedTimeRelativeErrorOption);

    CommonClRefoldingOptions commonClRefoldingOptions = CommonClRefoldingOptions.create();
    commonClRefoldingOptions.addTo(options);

    Option junctionPointsDirOption = JpHelper.createJunctionPointsOption();
    options.addOption(junctionPointsDirOption);

    Option contactPointAndSlopeDirOption = ContactPointAndSlopeHelper.createContactPointAndSlopeDirOption();
    options.addOption(contactPointAndSlopeDirOption);

    Option peakSlopesDirOption = PeakSlopesHelper.createPeakSlopesDirOption();
    options.addOption(peakSlopesDirOption);

    Parser parser = new GnuParser();
    CommandLine line = parser.parse(options, args);

    Path captureDir = commonRefoldingOptions.getCaptureDirectory(line);
    Path targetDir = commonRefoldingOptions.getTargetDirectory(line);

    RefoldingUtil.checkCaptureDirIsEmpty(captureDir);
    CommonThings.createDirectoryPathIfNeeded(targetDir);

    Path junctionPointsDir = JpHelper.createJpDirIfNeeded(junctionPointsDirOption, line);
    Path contactPointAndSlopeDir = ContactPointAndSlopeHelper.createCpsDirIfNeeded(
      contactPointAndSlopeDirOption, line);
    Path peakSlopesDir = PeakSlopesHelper.createPeakSlopesDirIfNeeded(peakSlopesDirOption, line);
    boolean asyncSvg = Boolean.parseBoolean(line.getOptionValue(asyncSvgOption.getOpt()).toLowerCase(
      Locale.ROOT));
    
    String defLsbVsZLsbSlopeString = line.getOptionValue(defLsbVsZLsbSlopeOption.getOpt());
    double slope = StringUtils.isEmpty(defLsbVsZLsbSlopeString) ? Double.NaN
      : Double.parseDouble(defLsbVsZLsbSlopeString);
    long nanoDriverDelayMillis = commonRefoldingOptions.getNanoDriverDelayMillis(line);
    long sleepMillis = commonRefoldingOptions.getSleepMillis(line);
    long waitWhileEmptyMillis = commonRefoldingOptions.getWaitWhileEmptyMillis(line);
    FileMovingWatcher fileMovingWatcher = new FileMovingWatcher(
      waitWhileEmptyMillis, sleepMillis, Sleeper.SYSTEM_SLEEPER, captureDir, targetDir);
    
    boolean forceRecord = commonRefoldingOptions.isForceRecord(line);
    String nanoDriverClassname = commonRefoldingOptions.getNanoDriverClassname(line);
    String nanoDriverHost = commonRefoldingOptions.getNanoDriverHost(line);
    int nanoDriverPort = commonRefoldingOptions.getNanoDriverPort(line);

    double temperatureK = analysisOptions.getTemperatureK(line);
    double persistenceLengthM = analysisOptions.persistenceLengthM(line);

    NanoDriverConfig config = PicoUtil.isFakeOrRemoteNanoDriver(nanoDriverClassname) ? null
      : NanoDriverUtil.createFetchRefoldBreakRobotConfig(forceRecord);
    NanoDriver nanoDriver = DelayedNanoDriverInvocationHandler.wrapIfNecessary(
      PicoUtil.nanoDriver(nanoDriverClassname, config, nanoDriverHost, nanoDriverPort),
      nanoDriverDelayMillis);
    Sleeper nanoDriverSleeper = commonRefoldingOptions.getNanoDriverSleeper(line).make(nanoDriver);

    double svgDefScale = Double.parseDouble(line.getOptionValue(svgDefScaleOption.getOpt()));

    Path svgDir = analysisOptions.svgDir(line);
    if (svgDir != null) {
      CommonThings.createDirectoryPathIfNeeded(svgDir);
    }

    Path infoDir = analysisOptions.infoDir(line);
    if (infoDir != null) {
      CommonThings.createDirectoryPathIfNeeded(infoDir);
    }

    double initialStartRampM = Double.parseDouble(line.getOptionValue(initialStartRampNmOption.getOpt())) / 1e9;
    final double pushM = Double.parseDouble(line.getOptionValue(pushNmOption.getOpt())) / 1e9;

    FetchRefoldBreakParams fetchRefoldBreakParams = analysisOptions.fetchRefoldBreakParams(line);
    DoubleList allowedTimeRelativeError = AnalysisOptions.allowedTimeRelativeError(line, allowedTimeRelativeErrorOption);

    final double hoverM = fetchRefoldBreakParams.fetchRampSizeM - fetchRefoldBreakParams.refoldingApproachM - pushM;
    logger.log(Level.INFO, String.format(Locale.ROOT, "desiredPushingNm=%g hoverNm=%s", pushM * 1e9, hoverM * 1e9));

    double runScriptExtraDelayS = Double.parseDouble(line.getOptionValue(runScriptExtraDelayMillisOption.getOpt())) / 1e3;
    Function<? super Double, Double> minPeakForceN = analysisOptions.minPeakForceN(line);
    double minPeakContourLengthM = analysisOptions.minPeakContourLengthM(line);
    double minContourLengthDeltaM = analysisOptions.minContourLengthDeltaM(line);
    double minPeakForceRelativeToEmpiricalBaselineN = analysisOptions.minPeakForceRelativeToEmpiricalBaselineN(line);
    Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN = analysisOptions.medianAbsoluteDeviationLowerBoundN(line);
    double leftMostlyHigherWindowSizeM = analysisOptions.leftMostlyHigherWindowSizeM(line);
    double leftMostlyHigherFraction = analysisOptions.leftMostlyHigherFraction(line);
    double leftMostlyHigherN = analysisOptions.leftMostlyHigherN(line);
    Function<? super Double, Double> rightMuchHigherThanAllN = analysisOptions.rightMuchHigherThanAllN(line);
    Function<? super Double, Double> rightMuchHigherN = analysisOptions.rightMuchHigherN(line);
    double atMostOnePeakWindowM = analysisOptions.atMostOnePeakWindowM(line);
    double linkerM = analysisOptions.linkerM(line);
    double linkerPerModuleM = analysisOptions.linkerPerModuleM(line);
    int maxRequiredWellSpacedAtEnd = analysisOptions.getMaxRequiredWellSpacedAtEnd(line);

    PathMatcher debugPathMatcher = targetDir.getFileSystem().getPathMatcher(commonRefoldingOptions.getDebugPattern(line));
    
    Preconditions.checkArgument(pushM > 0);
    // Initially assume we're pushing the right amount.
    FetchRefoldBreakState state = new FetchRefoldBreakState(initialStartRampM, pushM);

    double clRangeStartM = commonClRefoldingOptions.clRangeStartM(line);
    double clRangeEndM = commonClRefoldingOptions.clRangeEndM(line);
    boolean debug = commonRefoldingOptions.isDebug(line);
    // Only one thread.
    Executor svgExecutor = asyncSvg ? new ThreadPoolExecutor(
      1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(128),
      new ThreadPoolExecutor.CallerRunsPolicy() {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
          // For some reason, {0} doesn't work here.
          logger.log(Level.INFO, "Execution rejected so running in caller's thread: " + Thread.currentThread());
          super.rejectedExecution(r, e);
        }        
      }) : CommonThings.callerRunsExecutor;
    double expectedTimeS = fetchRefoldBreakParams.expectedTimeS();
    final double scriptTimeS = runScriptExtraDelayS + expectedTimeS;
    logger.log(
      Level.INFO, "scriptTimeS={0} runScriptExtraDelayS={1} expectedTimeS={2}",
      new Object[] {scriptTimeS, runScriptExtraDelayS, expectedTimeS});
    // The first time, let's quit the strip-chart mode if it was interrupted previously.
    nanoDriver.quitStripChart();
    while (true) {
      double startRampM = state.startRampM + pushM - state.pushingM;
      nanoDriver.startRampColumn1(startRampM);
      nanoDriver.endRampColumn1(startRampM + fetchRefoldBreakParams.fetchRampSizeM);
      TimedFileContentAndTargetFile fileContentAndTargetFile = RefoldingUtil.executeScript(
        "", "", scriptTimeS, nanoDriverSleeper, false, nanoDriver, fileMovingWatcher);
      // TODO: Look only at the last fileContentAndTargetFile.upperBoundTimeS period.
      double pushingM = StripChartRefolding.process(
        junctionPointsDir, slope, contactPointAndSlopeDir, peakSlopesDir, svgDefScale, svgDir, infoDir,
        fetchRefoldBreakParams, allowedTimeRelativeError, minPeakForceN, minPeakContourLengthM, minContourLengthDeltaM,
        minPeakForceRelativeToEmpiricalBaselineN, medianAbsoluteDeviationLowerBoundN,
        leftMostlyHigherWindowSizeM, leftMostlyHigherFraction, leftMostlyHigherN,
        rightMuchHigherThanAllN, rightMuchHigherN, atMostOnePeakWindowM, linkerM, linkerPerModuleM,
        maxRequiredWellSpacedAtEnd, clRangeStartM, clRangeEndM, Wlc.KB * temperatureK
          / persistenceLengthM, debug, debugPathMatcher, fileContentAndTargetFile.fileContentAndTargetFile, svgExecutor);
      state = new FetchRefoldBreakState(startRampM, Double.isNaN(pushingM) ? state.pushingM
        : pushingM);
    }
  }

  private static class FetchRefoldBreakState {
    private final double startRampM;
    private final double pushingM;

    public FetchRefoldBreakState(double startRampM, double pushingM) {
      this.startRampM = startRampM;
      this.pushingM = pushingM;
    }

    @Override
    public String toString() {
      return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
  }
}
